/*
 * Unidata Platform Community Edition
 * Copyright (c) 2013-2020, UNIDATA LLC, All rights reserved.
 * This file is part of the Unidata Platform Community Edition software.
 *
 * Unidata Platform Community Edition is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Unidata Platform Community Edition is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 */
package org.unidata.mdm.meta.service.impl.data;

import java.time.OffsetDateTime;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import java.util.stream.Collectors;

import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.stereotype.Component;
import org.unidata.mdm.core.context.ModelChangeContext;
import org.unidata.mdm.core.context.ModelChangeContext.ModelChangeType;
import org.unidata.mdm.core.context.ModelGetContext;
import org.unidata.mdm.core.context.ModelRefreshContext;
import org.unidata.mdm.core.context.ModelRemoveContext;
import org.unidata.mdm.core.context.ModelSourceContext;
import org.unidata.mdm.core.service.MetaModelService;
import org.unidata.mdm.core.service.ModelRefreshListener;
import org.unidata.mdm.core.service.impl.AbstractModelComponent;
import org.unidata.mdm.core.type.model.EntityElement;
import org.unidata.mdm.core.type.model.ModelDescriptor;
import org.unidata.mdm.core.type.model.ModelImplementation;
import org.unidata.mdm.core.type.model.ModelSource;
import org.unidata.mdm.core.util.SecurityUtils;
import org.unidata.mdm.draft.context.DraftGetContext;
import org.unidata.mdm.draft.context.DraftUpsertContext;
import org.unidata.mdm.draft.dto.DraftGetResult;
import org.unidata.mdm.draft.service.DraftService;
import org.unidata.mdm.draft.type.Draft;
import org.unidata.mdm.draft.type.Edition;
import org.unidata.mdm.meta.configuration.Descriptors;
import org.unidata.mdm.meta.configuration.TypeIds;
import org.unidata.mdm.meta.context.GetDataModelContext;
import org.unidata.mdm.meta.context.SourceDataModelContext;
import org.unidata.mdm.meta.context.UpsertDataModelContext;
import org.unidata.mdm.meta.dao.DataDAO;
import org.unidata.mdm.meta.dto.GetEntitiesGroupsDTO;
import org.unidata.mdm.meta.dto.GetEntityDTO;
import org.unidata.mdm.meta.dto.GetLookupDTO;
import org.unidata.mdm.meta.dto.GetModelDTO;
import org.unidata.mdm.meta.dto.GetModelRelationDTO;
import org.unidata.mdm.meta.dto.GetNestedEntitiesResult;
import org.unidata.mdm.meta.exception.MetaExceptionIds;
import org.unidata.mdm.meta.exception.ModelRuntimeException;
import org.unidata.mdm.meta.exception.ModelValidationException;
import org.unidata.mdm.meta.po.DataPO;
import org.unidata.mdm.meta.serialization.MetaSerializer;
import org.unidata.mdm.meta.service.impl.data.instance.DataModelInstanceImpl;
import org.unidata.mdm.meta.service.impl.data.instance.EntitiesGroupImpl;
import org.unidata.mdm.meta.service.impl.data.instance.LookupImpl;
import org.unidata.mdm.meta.service.impl.data.instance.NestedImpl;
import org.unidata.mdm.meta.service.impl.data.instance.RegisterImpl;
import org.unidata.mdm.meta.service.impl.data.instance.RelationImpl;
import org.unidata.mdm.meta.service.impl.data.refresh.MappingUpdatesRefreshListener;
import org.unidata.mdm.meta.service.impl.data.refresh.SecurityResourcesRefreshListener;
import org.unidata.mdm.meta.service.impl.data.validation.DataModelValidationComponent;
import org.unidata.mdm.meta.type.draft.ModelDraftConstants;
import org.unidata.mdm.meta.type.instance.DataModelInstance;
import org.unidata.mdm.meta.type.model.DataModel;
import org.unidata.mdm.meta.type.model.entities.EntitiesGroup;
import org.unidata.mdm.meta.type.model.entities.Entity;
import org.unidata.mdm.meta.type.model.entities.LookupEntity;
import org.unidata.mdm.meta.type.model.entities.NestedEntity;
import org.unidata.mdm.meta.type.model.entities.Relation;
import org.unidata.mdm.meta.util.ModelUtils;
import org.unidata.mdm.system.exception.ValidationResult;
import org.unidata.mdm.system.service.AfterModuleStartup;

/**
 * @author Mikhail Mikhailov on Oct 13, 2020
 */
@Component(TypeIds.DATA_MODEL)
public class DataModelComponent extends AbstractModelComponent implements ModelImplementation<DataModelInstance>, AfterModuleStartup {

    private static final String ROOT_GROUP_NAME = "ROOT";

    /**
     * Model DAO.
     */
    @Autowired
    private DataDAO modelsDAO;
    /**
     * The MMS.
     */
    private MetaModelService metaModelService;
    /**
     * The DS.
     */
    @Autowired
    private DraftService draftService;
    /**
     * The validation component.
     */
    @Autowired
    private DataModelValidationComponent dataModelValidationComponent;
    /**
     * SRRL.
     */
    @Autowired
    private SecurityResourcesRefreshListener securityResourcesRefreshListener;
    /**
     * MURL.
     */
    @Autowired
    private MappingUpdatesRefreshListener mappingUpdatesRefreshListener;
    /**
     * Model instances, keyed by storage ID.
     */
    private final Map<String, DataModelInstance> instances = new ConcurrentHashMap<>(4);
    /**
     * Constructor.
     */
    @Autowired
    public DataModelComponent(MetaModelService metaModelService) {
        super();
        this.metaModelService = metaModelService;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public void afterModuleStartup() {
        // Source systems and enumerations must start prior to this model
        metaModelService.register(this);
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public Collection<ModelRefreshListener> listeners() {
        return List.of(securityResourcesRefreshListener, mappingUpdatesRefreshListener);
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public ModelDescriptor<DataModelInstance> descriptor() {
        return Descriptors.DATA;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public DataModelInstance instance(String storageId, String id) {
        return instances.computeIfAbsent(storageId, this::load);
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public GetModelDTO get(ModelGetContext ctx) {

        GetDataModelContext get = narrow(ctx);

        if (get.isDraftOperation()) {

            Long draftId = get.getDraftId();
            Long parentDraftId = get.getParentDraftId();

            DraftGetResult result = draftService.get(DraftGetContext.builder()
                    .draftId(draftId)
                    .parentDraftId(parentDraftId)
                    .payload(get)
                    .build());

            return result.hasPayload() ? result.narrow() : null;
        }

        return disassemble(instance(SecurityUtils.getStorageId(get), null), get);
    }

    public GetModelDTO disassemble(DataModelInstance i, GetDataModelContext ctx) {

        GetModelDTO result = new GetModelDTO();

        processEntities(i, ctx, result);
        processNested(i, ctx, result);
        processLookups(i, ctx, result);
        processRelations(i, ctx, result);
        processEntityGroups(i, ctx, result);
        processInfoFields(i, ctx, result);

        return result;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public void upsert(ModelChangeContext ctx) {

        UpsertDataModelContext change = narrow(ctx);

        // 1. Process draft and return
        if (change.isDraftOperation()) {

            Long draftId = change.getDraftId();
            Long parentDraftId = change.getParentDraftId();

            draftService.upsert(DraftUpsertContext.builder()
                    .draftId(draftId)
                    .parentDraftId(parentDraftId)
                    .payload(change)
                    .build());

            return;
        }

        // 2. Run direct upsert
        DataModel target = assemble(change);

        List<ValidationResult> validations = new ArrayList<>();
        validations.addAll(validate(target));
        validations.addAll(metaModelService.allow(SourceDataModelContext.builder()
                    .source(target)
                    .build()));

        if (CollectionUtils.isNotEmpty(validations)) {
            throw new ModelValidationException("Cannot upsert data model. Validation errors exist.",
                    MetaExceptionIds.EX_META_UPSERT_DATA_MODEL_VALIDATION, validations);
        }

        put(SecurityUtils.getStorageId(change), target, change.force());
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public DataModel assemble(ModelChangeContext ctx) {

        UpsertDataModelContext change = narrow(ctx);
        String storageId = SecurityUtils.getStorageId(change);

        DataModelInstance current = instance(storageId, null);

        DataModel source;
        DataModel target;
        Integer next;

        // 1. Processing draft
        if (change.isDraftOperation()) {

            // Must be set!
            Draft draft = change.currentDraft();

            // 1.2. Load last edition
            Edition latest = null;
            if (draft.isExisting()) {
                latest = draftService.current(draft.getDraftId(), true);
            }

            // 1.3. Take data snapshot for new drafts, if no editions exist
            if (Objects.isNull(latest)) {
                source = current.toSource();
                next = current.getVersion() + 1;
            // 1.4. Use previous revision data otherwise
            } else {
                source = latest.getContent();
                next = draft.getVariables().<Integer>valueGet(ModelDraftConstants.DRAFT_START_VERSION) + 1;
            }

        // 2. Direct upsert
        } else {
            source = change.getUpsertType() == ModelChangeType.FULL ? null : current.toSource();
            next = Objects.nonNull(change.getVersion()) ? change.getVersion() : current.getVersion() + 1;
        }

        target = new DataModel()
                .withVersion(next)
                .withStorageId(storageId)
                .withCreateDate(OffsetDateTime.now())
                .withCreatedBy(SecurityUtils.getCurrentUserName());

        // 3. Process
        if (change.getUpsertType() == ModelChangeType.FULL) {
            target
              .withEntities(change.getEntitiesUpdate().stream().map(e -> e.withVersion(next)).collect(Collectors.toList()))
              .withLookupEntities(change.getLookupEntitiesUpdate().stream().map(e -> e.withVersion(next)).collect(Collectors.toList()))
              .withNestedEntities(change.getNestedEntitiesUpdate().stream().map(e -> e.withVersion(next)).collect(Collectors.toList()))
              .withRelations(change.getRelationsUpdate().stream().map(e -> e.withVersion(next)).collect(Collectors.toList()))
              .withEntitiesGroup(Optional.ofNullable(change.getEntitiesGroupsUpdate()).map(e -> e.withVersion(next)).orElse(null));
        } else {
            merge(target, source, change);
        }

        // 4. Info fields
        processInfoFields(target, change);

        // 5. Pre-check before validation to detect serious violations, rendering model unusable.
        Collection<ValidationResult> validations = dataModelValidationComponent.precheck(target);
        if (CollectionUtils.isNotEmpty(validations)) {
            throw new ModelValidationException("Cannot save data model. Pre-check validation errors exist.",
                    MetaExceptionIds.EX_META_UPSERT_DATA_MODEL_CONSISTENCY, validations);
        }

        return target;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public Collection<ValidationResult> validate(ModelSource source) {
        return dataModelValidationComponent.validate((DataModel) source);
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public Collection<ValidationResult> allow(ModelSourceContext<?> source) {
        return dataModelValidationComponent.allow(source);
    }
    /**
     * Puts a model to storage.
     * @param storageId the target storage id
     * @param src the model source
     * @param force force to latest version upon failure
     */
    public void put(String storageId, ModelSource src, boolean force) {

        Objects.requireNonNull(storageId, "Storage id must not be null.");
        DataModel source = narrow(src);

        DataPO po = new DataPO();
        po.setCreatedBy(SecurityUtils.getCurrentUserName());
        po.setStorageId(storageId);
        po.setRevision(source.getVersion());
        po.setContent(MetaSerializer.modelToCompressedXml(source));

        boolean running = true;
        while (running) {

            try {
                modelsDAO.save(po);
            } catch (DuplicateKeyException dke) {

                if (force) {

                    int latest = modelsDAO.latest(storageId);
                    int current = source.getVersion();
                    int next = latest + 1;

                    source.getEntities().stream()
                        .filter(e -> Objects.nonNull(e.getVersion()) && e.getVersion() == current)
                        .forEach(e -> e.withVersion(next));

                    source.getLookupEntities().stream()
                        .filter(e -> Objects.nonNull(e.getVersion()) && e.getVersion() == current)
                        .forEach(e -> e.withVersion(next));

                    source.getNestedEntities().stream()
                        .filter(e -> Objects.nonNull(e.getVersion()) && e.getVersion() == current)
                        .forEach(e -> e.withVersion(next));

                    source.getRelations().stream()
                        .filter(e -> Objects.nonNull(e.getVersion()) && e.getVersion() == current)
                        .forEach(e -> e.withVersion(next));

                    Optional.ofNullable(source.getEntitiesGroup())
                        .filter(g -> Objects.nonNull(g.getVersion()) && g.getVersion() == current)
                        .ifPresent(g -> g.withVersion(next));

                    source.withVersion(next);

                    po.setRevision(next);
                    po.setContent(MetaSerializer.modelToCompressedXml(source));

                    continue;
                } else {
                    throw new ModelRuntimeException(
                            "Cannot save data model. Revisions conflict [expected next {}].",
                            dke,
                            MetaExceptionIds.EX_META_UPSERT_DATA_MODEL_REVISION_EXISTS,
                            po.getRevision());
                }
            }

            running = false;
        }
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public void refresh(ModelRefreshContext refresh) {
        instances.computeIfPresent(SecurityUtils.getStorageId(refresh), (k, v) -> load(k));
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public void remove(ModelRemoveContext remove) {
        // Not supported
    }

    private DataModel narrow(ModelSource src) {

        Objects.requireNonNull(src, "Data model source input must not be null.");
        if (!StringUtils.equals(src.getTypeId(), TypeIds.DATA_MODEL)) {
            throw new ModelRuntimeException("Wrong input type [{}] for data model source operation, [{}] expected.",
                    MetaExceptionIds.EX_META_DATA_MODEL_SOURCE_WRONG_TYPE,
                    src.getTypeId(), TypeIds.DATA_MODEL);
        }

        return (DataModel) src;
    }

    private GetDataModelContext narrow(ModelGetContext ctx) {

        Objects.requireNonNull(ctx, "Data model get input must not be null.");
        if (!StringUtils.equals(ctx.getTypeId(), TypeIds.DATA_MODEL)) {
            throw new ModelRuntimeException("Wrong input type [{}] for data model get operation, [{}] expected.",
                    MetaExceptionIds.EX_META_DATA_MODEL_GET_WRONG_TYPE,
                    ctx.getTypeId(), TypeIds.DATA_MODEL);
        }

        return (GetDataModelContext) ctx;
    }

    private UpsertDataModelContext narrow(ModelChangeContext ctx) {

        Objects.requireNonNull(ctx, "Data model change input must not be null.");
        if (!StringUtils.equals(ctx.getTypeId(), TypeIds.DATA_MODEL)) {
            throw new ModelRuntimeException("Wrong input type [{}] for data model change operation, [{}] expected.",
                    MetaExceptionIds.EX_META_DATA_MODEL_CHANGE_WRONG_TYPE,
                    ctx.getTypeId(), TypeIds.DATA_MODEL);
        }

        return (UpsertDataModelContext) ctx;
    }

    private void merge(DataModel target, DataModel source, UpsertDataModelContext change) {

        // 0. The version must be already set
        int next = target.getVersion();

        // 1. Source
        Map<String, Entity> entitiesMerge = source
                .getEntities()
                .stream()
                .collect(Collectors.toMap(Entity::getName, Function.identity()));

        Map<String, LookupEntity> lookupsMerge = source
                .getLookupEntities()
                .stream()
                .collect(Collectors.toMap(LookupEntity::getName, Function.identity()));

        Map<String, NestedEntity> nestedMerge = source
                .getNestedEntities()
                .stream()
                .collect(Collectors.toMap(NestedEntity::getName, Function.identity()));

        Map<String, Relation> relationsMerge = source
                .getRelations()
                .stream()
                .collect(Collectors.toMap(Relation::getName, Function.identity()));

        // 2. Updates
        if (change.hasEntitiesUpdate()) {
            entitiesMerge.putAll(change.getEntitiesUpdate().stream()
                .map(e -> e.withVersion(next))
                .collect(Collectors.toMap(Entity::getName, Function.identity())));
        }

        if (change.hasLookupEntitiesUpdate()) {
            lookupsMerge.putAll(change.getLookupEntitiesUpdate().stream()
                .map(e -> e.withVersion(next))
                .collect(Collectors.toMap(LookupEntity::getName, Function.identity())));
        }

        if (change.hasNestedEntitiesUpdate()) {
            nestedMerge.putAll(change.getNestedEntitiesUpdate().stream()
                .map(e -> e.withVersion(next))
                .collect(Collectors.toMap(NestedEntity::getName, Function.identity())));
        }

        if (change.hasRelationsUpdate()) {
            relationsMerge.putAll(change.getRelationsUpdate().stream()
                .map(e -> e.withVersion(next))
                .collect(Collectors.toMap(Relation::getName, Function.identity())));
        }

        // 3. Deletes
        if (change.hasEntitiesDelete()) {
            change.getEntitiesDelete().forEach(entitiesMerge::remove);
        }

        if (change.hasLookupEntitiesDelete()) {
            change.getLookupEntitiesDelete().forEach(lookupsMerge::remove);
        }

        if (change.hasNestedEntitiesDelete()) {
            change.getNestedEntitiesDelete().forEach(nestedMerge::remove);
        }

        if (change.hasRelationsDelete()) {
            change.getRelationsDelete().forEach(relationsMerge::remove);
        }

        // 4. Groups
        // Now change groups without pre-validation by just overwriting
        // existing groups. This has the effect, that validations will fail on publication.
        EntitiesGroup root = source.getEntitiesGroup();
        if (change.hasEntitiesGroupUpdate()) {
            root = merge(change.getEntitiesGroupsUpdate(), entitiesMerge, lookupsMerge)
                    .withVersion(next);
        }

        // 4. Set
        target
            .withEntities(entitiesMerge.values())
            .withLookupEntities(lookupsMerge.values())
            .withNestedEntities(nestedMerge.values())
            .withRelations(relationsMerge.values())
            .withEntitiesGroup(root);
    }

    private EntitiesGroup merge(EntitiesGroup update, Map<String, Entity> entitiesMerge, Map<String, LookupEntity> lookupsMerge) {

        Set<String> groupNames = updateGroupNameSet(update, ROOT_GROUP_NAME, new HashSet<>());
        Queue<Pair<String, EntitiesGroup>> queue = new LinkedList<>();
        queue.add(Pair.of(StringUtils.EMPTY, update));

        while (!queue.isEmpty()) {

            Pair<String, EntitiesGroup> each = queue.poll();

            // 1. Possibly update the group name in lookup objects.
            lookupsMerge.values().stream()
                .filter(x -> !groupNames.contains(x.getGroupName()))
                .forEach(x -> x.setGroupName(ROOT_GROUP_NAME));

            each.getRight().getMappedLookups().stream()
                .map(lookupsMerge::get)
                .filter(Objects::nonNull)
                .forEach(lookup -> lookup.setGroupName(ModelUtils.joinPath(each.getKey(), each.getRight().getName())));

            // 2. Possibly update the group name in register objects.
            entitiesMerge.values().stream()
                .filter(x -> !groupNames.contains(x.getGroupName()))
                .forEach(x -> x.setGroupName(ROOT_GROUP_NAME));

            each.getRight().getMappedRegisters().stream()
                .map(entitiesMerge::get)
                .filter(Objects::nonNull)
                .forEach(register -> register.setGroupName(ModelUtils.joinPath(each.getKey(), each.getRight().getName())));

            List<EntitiesGroup> inner = each.getRight().getInnerGroups();
            if (CollectionUtils.isNotEmpty(inner)) {
                String prefix = ModelUtils.joinPath(each.getKey(), each.getRight().getName());
                queue.addAll(inner.stream().map(i -> Pair.of(prefix, i)).collect(Collectors.toList()));
            }
        }

        return update;
    }

    private Set<String> updateGroupNameSet(EntitiesGroup update, String prefix, Set<String> collector) {
        List<EntitiesGroup> innerGroups = ObjectUtils.defaultIfNull(update.getInnerGroups(), Collections.emptyList());
        for (EntitiesGroup each : innerGroups) {
            String key = StringUtils.isBlank(prefix) ? each.getName() : prefix + "." + each.getName();
            collector.add(key);
            updateGroupNameSet(each, key, collector);
        }
        return collector;
    }

    private DataModelInstance load(String storageId) {

        DataPO po = modelsDAO.current(storageId);
        if (Objects.isNull(po) || ArrayUtils.isEmpty(po.getContent())) {
            return new DataModelInstanceImpl(
                    new DataModel()
                        .withEntitiesGroup(ModelUtils.createDefaultEntitiesGroup())
                        .withVersion(0)
                        .withStorageId(storageId),
                    metaModelService.instance(Descriptors.SOURCE_SYSTEMS));
        }

        return new DataModelInstanceImpl(
                MetaSerializer.modelFromCompressedXml(po.getContent()),
                metaModelService.instance(Descriptors.SOURCE_SYSTEMS));
    }

    private void processEntities(DataModelInstance i, GetDataModelContext ctx, GetModelDTO dto) {

        if (!ctx.isAllEntities() && CollectionUtils.isEmpty(ctx.getEntityIds())) {
            return;
        }

        List<GetEntityDTO> entities;
        if (ctx.isAllEntities()) {
            entities = i.getRegisters().stream()
                    .map(el -> {
                        Entity e = ((RegisterImpl) el).getSource();
                        if (ctx.isReduced()) {
                            return new GetEntityDTO(e);
                        }

                        List<NestedEntity> nested = el.getRegister().getReferencedNesteds().stream()
                            .map(ne -> ((NestedImpl) ne).getSource())
                            .collect(Collectors.toList());

                        List<Relation> ors = el.getRegister().getOutgoingRelations().keySet().stream()
                            .map(or -> ((RelationImpl) or).getSource())
                            .collect(Collectors.toList());

                        List<Relation> irs = el.getRegister().getIncomingRelations().keySet().stream()
                                .map(or -> ((RelationImpl) or).getSource())
                                .collect(Collectors.toList());

                        return new GetEntityDTO(e, nested, ors, irs);
                    })
                    .collect(Collectors.toList());

        } else {
            entities = ctx.getEntityIds().stream()
                    .map(i::getRegister)
                    .filter(Objects::nonNull)
                    .map(el -> {
                        Entity e = ((RegisterImpl) el).getSource();
                        if (ctx.isReduced()) {
                            return new GetEntityDTO(e);
                        }

                        List<NestedEntity> nested = el.getRegister().getReferencedNesteds().stream()
                            .map(ne -> ((NestedImpl) ne).getSource())
                            .collect(Collectors.toList());

                        List<Relation> ors = el.getRegister().getOutgoingRelations().keySet().stream()
                            .map(or -> ((RelationImpl) or).getSource())
                            .collect(Collectors.toList());

                        List<Relation> irs = el.getRegister().getIncomingRelations().keySet().stream()
                                .map(or -> ((RelationImpl) or).getSource())
                                .collect(Collectors.toList());

                        return new GetEntityDTO(e, nested, ors, irs);
                    })
                    .collect(Collectors.toList());
        }

        dto.setEntities(entities);
    }

    private void processLookups(DataModelInstance i, GetDataModelContext ctx, GetModelDTO dto) {

        if (!ctx.isAllLookups() && CollectionUtils.isEmpty(ctx.getLookupIds())) {
            return;
        }

        List<GetLookupDTO> entities;
        if (ctx.isAllLookups()) {
            entities = i.getLookups().stream()
                    .map(el -> new GetLookupDTO(((LookupImpl) el).getSource()))
                    .collect(Collectors.toList());
        } else {
            entities = ctx.getLookupIds().stream()
                    .map(i::getLookup)
                    .filter(Objects::nonNull)
                    .map(el -> new GetLookupDTO(((LookupImpl) el).getSource()))
                    .collect(Collectors.toList());
        }

        dto.setLookups(entities);
    }

    private void processRelations(DataModelInstance i, GetDataModelContext ctx, GetModelDTO dto) {

        if (!ctx.isAllRelations()
         && CollectionUtils.isEmpty(ctx.getRelationIds())
         && !((ctx.isAllFromRelations() || ctx.isAllToRelations()) && CollectionUtils.isNotEmpty(ctx.getEntityIds()))) {
            return;
        }

        List<GetModelRelationDTO> relations;
        if (ctx.isAllRelations()) {
            relations = i.getRelations().stream()
                    .map(el -> new GetModelRelationDTO(((RelationImpl) el).getSource()))
                    .collect(Collectors.toList());
        } else if ((ctx.isAllFromRelations() || ctx.isAllToRelations()) && CollectionUtils.isNotEmpty(ctx.getEntityIds())) {
            relations = ctx.getEntityIds().stream()
                    .map(i::getElement)
                    .filter(EntityElement::isRegister)
                    .map(EntityElement::getRegister)
                    .flatMap(el -> ctx.isAllFromRelations()
                            ? el.getOutgoingRelations().keySet().stream()
                            : el.getIncomingRelations().keySet().stream())
                    .map(re -> new GetModelRelationDTO(((RelationImpl) re).getSource()))
                    .collect(Collectors.toList());
        } else {
            relations = ctx.getRelationIds().stream()
                    .map(i::getRelation)
                    .filter(Objects::nonNull)
                    .map(el -> new GetModelRelationDTO(((RelationImpl) el).getSource()))
                    .collect(Collectors.toList());
        }

        dto.setRelations(relations);
    }

    private void processNested(DataModelInstance i, GetDataModelContext ctx, GetModelDTO dto) {

        if (!ctx.isAllNestedEntities() && CollectionUtils.isEmpty(ctx.getNestedEntityIds())) {
            return;
        }

        List<GetNestedEntitiesResult> nested;
        if (ctx.isAllNestedEntities()) {
            nested = i.getNested().stream()
                    .map(el -> new GetNestedEntitiesResult(((NestedImpl) el).getSource()))
                    .collect(Collectors.toList());
        } else {
            nested = ctx.getNestedEntityIds().stream()
                    .map(i::getNested)
                    .filter(Objects::nonNull)
                    .map(el -> new GetNestedEntitiesResult(((NestedImpl) el).getSource()))
                    .collect(Collectors.toList());
        }

        dto.setNested(nested);
    }

    private void processEntityGroups(DataModelInstance i, GetDataModelContext ctx, GetModelDTO dto) {

        if (!ctx.isAllEntityGroups() && CollectionUtils.isEmpty(ctx.getEntityGroupIds())) {
            return;
        }

        Map<String, EntitiesGroup> groups = new HashMap<>();
        Map<EntitiesGroup, Pair<List<Entity>, List<LookupEntity>>> sets = new HashMap<>();

        if (ctx.isAllEntityGroups()) {
            i.getGroups().forEach(g -> {
                groups.put(g.getPath(), ((EntitiesGroupImpl) g).getSource());
                sets.put(((EntitiesGroupImpl) g).getSource(),
                        Pair.of(g.getRegisters().stream().map(el -> ((RegisterImpl) el).getSource()).collect(Collectors.toList()),
                                g.getLookups().stream().map(el -> ((LookupImpl) el).getSource()).collect(Collectors.toList())));
            });
        } else {
            ctx.getEntityGroupIds().stream()
                .map(i::getGroup)
                .filter(Objects::nonNull)
                .forEach(g -> {
                    groups.put(g.getPath(), ((EntitiesGroupImpl) g).getSource());
                    sets.put(((EntitiesGroupImpl) g).getSource(),
                            Pair.of(g.getRegisters().stream().map(el -> ((RegisterImpl) el).getSource()).collect(Collectors.toList()),
                                    g.getLookups().stream().map(el -> ((LookupImpl) el).getSource()).collect(Collectors.toList())));
                });
        }

        dto.setEntityGroups(new GetEntitiesGroupsDTO(groups, sets));
    }
}
